/*
 * MuSortApp.java
 */

package musort;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.logging.Level;
import org.jaudiotagger.audio.AudioFile;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.tag.Tag;
import org.jdesktop.application.Application;
import org.jdesktop.application.SingleFrameApplication;
import org.jdesktop.application.Task;
import org.ontoware.aifbcommons.collection.ClosableIterator;
import org.ontoware.rdf2go.RDF2Go;
import org.ontoware.rdf2go.exception.ModelException;
import org.ontoware.rdf2go.model.Model;
import org.ontoware.rdf2go.model.ModelSet;
import org.ontoware.rdf2go.model.Syntax;
import org.ontoware.rdf2go.model.node.URI;
import org.ontoware.rdf2go.model.node.impl.URIImpl;
import org.semanticdesktop.aperture.accessor.DataAccessor;
import org.semanticdesktop.aperture.accessor.DataObject;
import org.semanticdesktop.aperture.accessor.file.FileAccessor;
import org.semanticdesktop.aperture.accessor.impl.DefaultDataAccessorRegistry;
import org.semanticdesktop.aperture.crawler.Crawler;
import org.semanticdesktop.aperture.crawler.ExitCode;
import org.semanticdesktop.aperture.crawler.filesystem.FileSystemCrawler;
import org.semanticdesktop.aperture.datasource.filesystem.FileSystemDataSource;
import org.semanticdesktop.aperture.examples.handler.CrawlerHandlerBase;
import org.semanticdesktop.aperture.mime.identifier.MimeTypeIdentifier;
import org.semanticdesktop.aperture.mime.identifier.magic.MagicMimeTypeIdentifier;
import org.semanticdesktop.aperture.rdf.RDFContainer;
import org.semanticdesktop.aperture.rdf.impl.RDFContainerImpl;
import org.semanticdesktop.aperture.util.IOUtil;
import org.semanticdesktop.aperture.vocabulary.NIE;

/**
 * The main class of the application.
 */
public class MuSortApp extends SingleFrameApplication {

    /**
     * At startup create and show the main frame of the application.
     */
    @Override protected void startup() {
        show(new MuSortView(this));
    }

    /**
     * This method is to initialize the specified window by injecting resources.
     * Windows shown in our application come fully initialized from the GUI
     * builder, so this additional configuration is not needed.
     */
    @Override protected void configureWindow(java.awt.Window root) {
    }

    /**
     * A convenient static getter for the application instance.
     * @return the instance of MuSortApp
     */
    public static MuSortApp getApplication() {
        return Application.getInstance(MuSortApp.class);
    }

    /**
     * Main method launching the application.
     */
    public static void main(String[] args) {
        launch(MuSortApp.class, args);
    }
    
    /**
     * A Task that saves a text String to a file.  The file is not appended
     * to, its contents are replaced by the String.
     */
    static class SortMusicTask extends Task<Void, Void> {

        private final File fromfolder;
        private final File tofolder;
        private final Boolean createcopy;
        private final Boolean allowduplicates;

        /**
         * Construct a SaveTextFileTask.
         *
         * @param file The file to save to
         * @param text The new contents of the file
         */
        SortMusicTask(Application app, File fromfolder, File tofolder, Boolean createcopy, Boolean allowduplicates) {
            super(app);
            this.fromfolder = fromfolder;
            this.tofolder = tofolder;
            this.createcopy = createcopy;
            this.allowduplicates = allowduplicates;
        }

        /**
         * Return the File that the {@link #getText text} will be
         * written to.
         *
         * @return the value of the read-only file property.
         */
        public final File getFromFolder() {
            return fromfolder;
        }

        /**
         * Return the String that will be written to the
         * {@link #getFile file}.
         *
         * @return the value of the read-only text property.
         */
        public final File getToFolder() {
            return tofolder;
        }
        
        private void renameFile(File oldFile, File newFile) throws IOException {
            if (!oldFile.renameTo(newFile)) {
                String fmt = String.format("file rename failed: %s => %s",oldFile.getAbsolutePath(),newFile.getAbsolutePath());
                throw new IOException(String.format(fmt, oldFile, newFile));
            }
        }
        
        /**
         * Writes the {@code text} to the specified {@code file}.  The
         * implementation is conservative: the {@code text} is initially
         * written to ${file}.tmp, then the original file is renamed
         * ${file}.bak, and finally the temporary file is renamed to ${file}.
         * The Task's {@code progress} property is updated as the text is
         * written.
         * <p>
         * If this Task is cancelled before writing the temporary file
         * has been completed, ${file.tmp} is deleted.
         * <p>
         * The conservative algorithm for saving to a file was lifted from
         * the FileSaver class described by Ian Darwin here:
         * <a href="http://javacook.darwinsys.com/new_recipes/10saveuserdata.jsp">
         * http://javacook.darwinsys.com/new_recipes/10saveuserdata.jsp
         * </a>.
         *
         * @return null
         */
        @Override
        protected Void doInBackground() throws IOException {
            // create a model that will store the data source configuration
            Model model = RDF2Go.getModelFactory().createModel();
            // open the model
            model.open();
            // .. and wrap it in an RDFContainer
            RDFContainer configuration = new RDFContainerImpl(model, new URIImpl("source:testSource"), false);

            // now create the data source
            FileSystemDataSource source = new FileSystemDataSource();
            // and set the configuration container
            source.setConfiguration(configuration);
            // now we can call the type-specific setters in each DataSource class
            source.setRootFolder(getFromFolder().getAbsolutePath());

            // setup a crawler that can handle this type of DataSource
            FileSystemCrawler crawler = new FileSystemCrawler();
            crawler.setDataSource(source);
            crawler.setDataAccessorRegistry(new DefaultDataAccessorRegistry());
            try {
                crawler.setCrawlerHandler(new MuSortCrawlerHandler(this.fromfolder, this.tofolder, this.createcopy, this.allowduplicates));
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }

            // start crawling
            crawler.crawl();
            
            return null;
        }
    }
    
    static class MuSortCrawlerHandler extends CrawlerHandlerBase {

        // our 'persistent' modelSet
        private ModelSet modelSet;
        // create a MimeTypeIdentifier
        MimeTypeIdentifier identifier = new MagicMimeTypeIdentifier();
        // sort options
        private final File fromFolder;
        private final File toFolder;
        private final Boolean createcopy;
        private final Boolean allowduplicates;
        // recognised file counts
        int numunrecognised = 0;
        int numrecognised = 0;
        
        
        public MuSortCrawlerHandler(File fromFolder, File toFolder, Boolean createcopy, Boolean allowduplicates) throws ModelException {
            modelSet = RDF2Go.getModelFactory().createModelSet();
            modelSet.open();
            this.fromFolder = fromFolder;
            this.toFolder = toFolder;
            this.createcopy = createcopy;
            this.allowduplicates = allowduplicates;
        }
        
        private void copyOrMove(File fromfile) {
            try {
                String filename = fromfile.getName();
                String ext = (filename.lastIndexOf(".")==-1)?"":filename.substring(filename.lastIndexOf(".")+1,filename.length());
                AudioFile f = AudioFileIO.read(fromfile);
                Tag tag = f.getTag();
                String artist = tag.getFirstArtist();
                String title = tag.getFirstTitle();
                String album = tag.getFirstAlbum();
                File dir = new File(this.toFolder.getPath() + File.separator + artist + File.separator + album);
                dir.mkdirs();
                File tofile;
                tofile = new File(dir.getPath() + File.separator + artist + " - " + title + "." + ext);
                this.safeCopy(fromfile, tofile, this.allowduplicates);

                if (!this.createcopy) {
                    tofile.delete();
                }

                System.out.println("[" + ext.toUpperCase() + "]");
                System.out.println("     " + filename);
                filename = artist + " - " + title + "." + ext.toLowerCase();
                System.out.println("  -> " + filename);
            } catch (Exception e) {
                //Yeah yeah I know
            }
        }
        
        public static void safeCopy(File fromFile, File toFile, boolean allowduplicates)
            throws IOException {
                if (!fromFile.exists())
                    throw new IOException("FileCopy: " + "no such source file: " + fromFile.getName());

                if (!fromFile.isFile())
                    throw new IOException("FileCopy: " + "can't copy directory: " + fromFile.getName());

                if (!fromFile.canRead())
                    throw new IOException("FileCopy: " + "source file is unreadable: " + fromFile.getName());

                if (toFile.isDirectory())
                    toFile = new File(toFile, fromFile.getName());

                if (toFile.exists()) {
                    if (allowduplicates) {
                        int filecount = 1;
                        String origToFileName = new String(toFile.getName());
                        while (toFile.exists()) {
                            System.out.println("FileCopy: " + "attempt again as destination file exists: " + toFile.getName());
                            toFile = new File(toFile.getParent(),String.format("Duplicate %s of " + origToFileName,Integer.toString(filecount)));
                            filecount++;
                            if (filecount > 999) {
                                System.out.println("Assuming bad file. Skipping.");
                                toFile = new File(toFile.getParent(),origToFileName);
                                filecount = -1;
                                break;
                            }
                        }
                        if (filecount > 0)
                            MuSortCrawlerHandler.safeCopy(fromFile, toFile, allowduplicates);
                    }
                } else {
                    System.out.println("Y");
                    String parent = toFile.getParent();
                    if (parent == null)
                        parent = System.getProperty("user.dir");
                    File dir = new File(parent);
                    if (!dir.exists())
                        throw new IOException("FileCopy: " + "destination directory doesn't exist: " + parent);
                    if (dir.isFile())
                        throw new IOException("FileCopy: " + "destination is not a directory: " + parent);
                    if (!dir.canWrite())
                        throw new IOException("FileCopy: " + "destination directory is unwriteable: " + parent);
                }

                FileInputStream from = null;
                FileOutputStream to = null;

                try {
                    from = new FileInputStream(fromFile);
                    to = new FileOutputStream(toFile);
                    byte[] buffer = new byte[4096];
                    int bytesRead;

                    while ((bytesRead = from.read(buffer)) != -1)
                        to.write(buffer, 0, bytesRead); // write
                } finally {
                    if (from != null)
                    try {
                        from.close();
                    } catch (IOException e) {
                        ;
                    }
                    if (to != null)
                        try {
                            to.close();
                        } catch (IOException e) {
                            ;
                        }
                }
            }

        public void crawlStopped(Crawler crawler, ExitCode exitCode) {
            try {
                sortModelsetFiles();
            }
            catch (Exception e) {
                throw new RuntimeException(e);
            }
            finally {
                modelSet.close();
            }
        }

        public RDFContainer getRDFContainer(URI uri) {
            // we create a new in-memory temporary model for each data source
            Model model = RDF2Go.getModelFactory().createModel(uri);
            // A model needs to be opened before being wrapped in an RDFContainer
            model.open();
            return new RDFContainerImpl(model, uri);
        }

        public void objectNew(Crawler crawler, DataObject object) {
            try {
                // first we try to extract the information from the binary file
                processBinary(object);
                // read as many bytes of the file as desired by the MIME type identifier
                File file = new File(object.getID().asJavaURI().getPath());
                FileInputStream stream = new FileInputStream(file);
                BufferedInputStream buffer = new BufferedInputStream(stream);
                byte[] bytes = IOUtil.readBytes(buffer, identifier.getMinArrayLength());
                stream.close();

                String mimeType = identifier.identify(bytes, object.getID().asJavaURI().getPath(), null);
                // then we add this information to our persistent model
                Model objmod = object.getMetadata().getModel();
                objmod.open();
                URI uri = new URIImpl(file.toURI().toString());
                RDFContainer container = new RDFContainerImpl(objmod, uri);
                container.add(NIE.mimeType, mimeType);
                modelSet.addModel(objmod);
                container.getModel().writeTo(new PrintWriter(System.out),Syntax.Ntriples);
                numrecognised++;
            } catch (NoClassDefFoundError e) {
                numunrecognised++;
                System.out.println("Ignore this error, attempted to import nonexistant class.");
                System.out.println(e.getMessage());
            } catch (Exception e) {
                numunrecognised++;
                System.out.println(e.getMessage());
            }
            // don't forget to dispose of the DataObject
            object.dispose();
        }

        public void objectChanged(Crawler crawler, DataObject object) {
            // first we remove old information about the data object
            modelSet.removeModel(object.getID());
            // then we try to extract metadata and fulltext from the file
            processBinary(object);
            // an then we add the information from the temporary model to our
            // 'persistent' model
            modelSet.addModel(object.getMetadata().getModel());
            // don't forget to dispose of the DataObject
            object.dispose();
        }

        public void objectRemoved(Crawler crawler, URI uri) {
            // an object has been removed, we delete it from the rdf store
            modelSet.removeModel(uri);
        }
        
        private void sortModelsetFiles() {
            //modelSet.writeTo(System.out, Syntax.Trix);
            ClosableIterator it = modelSet.getModels();
            System.out.println("Test list:");

            DataAccessor accessor = new FileAccessor();

            Level templevel = AudioFileIO.logger.getParent().getLevel();
            AudioFileIO.logger.getParent().setLevel(Level.WARNING);
            AudioFile f;

            while (it.hasNext()) {
                Model m = (Model) it.next();
                m.open();
                RDFContainer container = new RDFContainerImpl(m, m.getContextURI());
                //System.out.println("[" + container.getString(NIE.mimeType) + "] ");
                // m.getContextURI().asJavaURI().getPath();

                if (container.getString(NIE.mimeType).equals("audio/mpeg")|container.getString(NIE.mimeType).equals("application/x-ogg")) {
                    try {
                        File file = new File(m.getContextURI().asJavaURI());
                        //Here is where we should perform the move
                        this.copyOrMove(file);
/*                        String filename = file.getName();
                        String ext = (filename.lastIndexOf(".")==-1)?"":filename.substring(filename.lastIndexOf(".")+1,filename.length());
                        f = AudioFileIO.read(file);
                        Tag tag = f.getTag();
                        String artist = tag.getFirstArtist();
                        String title = tag.getFirstTitle();
                        System.out.println("[" + ext.toUpperCase() + "]");
                        System.out.println("     " + filename);
                        filename = artist + " - " + title + "." + ext.toLowerCase();
                        System.out.println("  -> " + filename);
*/                        //AudioHeader ah = f.getAudioHeader();
                    } catch (Exception e) {
                        AudioFileIO.logger.log(Level.WARNING, "Couldn't read [" + container.getString(NIE.mimeType) + "]" + m.getContextURI().asJavaURI());
                    }
                } else {
                    System.out.println("[" + container.getString(NIE.mimeType) + "] ");
                }
                m.close();
            }

            AudioFileIO.logger.getParent().setLevel(templevel);
            it.close();
            System.out.println("\n----------------------------------------");
            System.out.println("Summary Statistics");
            System.out.println("  Recognised files:   " + String.valueOf(numrecognised-1));//minus starting directory
            System.out.println("  Unrecognised files: " + String.valueOf(numunrecognised));
            System.out.println("  Total files:        " + String.valueOf(numunrecognised+numrecognised-1));
            System.out.println("----------------------------------------");
        }
    }
}
